﻿namespace MvvmTools
{
    using System;
    using System.Diagnostics.Contracts;
    using System.Linq.Expressions;
    using JetBrains.Annotations;

    public sealed class EqualityComparer<TTarget> : IEqualityComparerEx<TTarget>
    {
        private const int InitialPrimeNumber = 17;
        private const int PrimeNumber = 31;
        private const string GetHashCodeFuncName = "GetHashCode";
        private static readonly ParameterExpression TargetParameter = Expression.Parameter(typeof(TTarget), Guid.NewGuid().ToString());
        private static readonly ParameterExpression OtherParameter = Expression.Parameter(typeof(TTarget), Guid.NewGuid().ToString());
        private readonly bool _isValueType;
        private Func<TTarget, TTarget, bool> _equFunc = (target, other) => false;
        private Expression _equalsExpression = Expression.Constant(true);
        private Expression _getHashCodeExpression = Expression.Constant(InitialPrimeNumber);
        private Func<TTarget, int> _getHashCodeFunc = target => 0;

        private EqualityComparer()
        {
            _isValueType = GetType().IsValueType;
        }

        public static IEqualityComparerBuilder<TTarget> Depends<TMember>(
            [NotNull] Expression<Func<TTarget, TMember>> accessor)
        {
            Contract.Requires<ArgumentNullException>(accessor != null);
            Contract.Ensures(Contract.Result<IEqualityComparerBuilder<TTarget>>() != null);

            var equality = new EqualityComparer<TTarget>();
            equality.AddDependency(accessor, false);
            return new EqualityComparerBuilder(equality);
        }

        public static IEqualityComparerBuilder<TTarget> DependsNullable<TMember>(
            [NotNull] Expression<Func<TTarget, TMember>> accessor)
            where TMember : class
        {
            Contract.Requires<ArgumentNullException>(accessor != null);
            Contract.Ensures(Contract.Result<IEqualityComparerBuilder<TTarget>>() != null);

            var equality = new EqualityComparer<TTarget>();
            equality.AddDependency(accessor, true);
            return new EqualityComparerBuilder(equality);
        }

        public bool Equals([CanBeNull] TTarget current, [CanBeNull] TTarget other)
        {
            return _equFunc(current, other);
        }

        public int GetHashCode([CanBeNull] TTarget current)
        {
            return _getHashCodeFunc(current);
        }

        public bool Equals(TTarget current, object obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }

            if (_isValueType)
            {
                return obj is TTarget && Equals((TTarget)obj);
            }

            if (ReferenceEquals(this, obj))
            {
                return true;
            }

            if (obj.GetType() != typeof(TTarget))
            {
                return false;
            }

            return Equals(current, (TTarget)obj);
        }

        private void AddDependency<TMember>(Expression<Func<TTarget, TMember>> accessor, bool canBeNull)
        {
            Contract.Requires<ArgumentNullException>(accessor != null);
            Expression targetMember = Expression.Invoke(accessor, TargetParameter);
            Expression getHashCode = Expression.Call(targetMember, typeof(TMember).GetMethod(GetHashCodeFuncName));
            if (canBeNull)
            {
                getHashCode = Expression.Condition(
                    Expression.Equal(
                        targetMember,
                        Expression.Constant(null)),
                    Expression.Constant(-1),
                    getHashCode);
            }

            _getHashCodeExpression = Expression.Add(
                Expression.Multiply(
                    Expression.Constant(PrimeNumber),
                    _getHashCodeExpression),
                getHashCode);

            Expression otherMember = Expression.Invoke(accessor, OtherParameter);
            _equalsExpression = Expression.And(_equalsExpression, Expression.Equal(targetMember, otherMember));
        }

        private void Build()
        {
            _getHashCodeFunc = Expression.Lambda<Func<TTarget, int>>(_getHashCodeExpression, TargetParameter).Compile();
            _equFunc = Expression.Lambda<Func<TTarget, TTarget, bool>>(_equalsExpression, TargetParameter, OtherParameter)
                    .Compile();
            _getHashCodeExpression = null;
            _equalsExpression = null;
        }

        private class EqualityComparerBuilder : IEqualityComparerBuilder<TTarget>
        {
            private readonly EqualityComparer<TTarget> _equalityComparer;
            private readonly Lazy<IEqualityComparerEx<TTarget>> _equalityVal;

            public EqualityComparerBuilder([NotNull] EqualityComparer<TTarget> equalityComparer)
            {
                Contract.Requires<ArgumentNullException>(equalityComparer != null);

                _equalityComparer = equalityComparer;
                _equalityVal = new Lazy<IEqualityComparerEx<TTarget>>(() =>
                {
                    equalityComparer.Build();
                    return equalityComparer;
                });
            }

            public IEqualityComparerEx<TTarget> Create()
            {
                Contract.Ensures(Contract.Result<IEqualityComparerEx<TTarget>>() != null);

                return _equalityVal.Value;
            }

            // ReSharper disable once MemberHidesStaticFromOuterClass
            public IEqualityComparerBuilder<TTarget> Depends<TMember>(Expression<Func<TTarget, TMember>> accessor)
            {
                Contract.Requires<ArgumentNullException>(accessor != null);
                Contract.Ensures(Contract.Result<IEqualityComparerBuilder<TTarget>>() != null);

                _equalityComparer.AddDependency(accessor, false);
                return this;
            }

            // ReSharper disable once MemberHidesStaticFromOuterClass
            public IEqualityComparerBuilder<TTarget> DependsNullable<TMember>(
                Expression<Func<TTarget, TMember>> accessor) where TMember : class
            {
                Contract.Requires<ArgumentNullException>(accessor != null);
                Contract.Ensures(Contract.Result<IEqualityComparerBuilder<TTarget>>() != null);

                _equalityComparer.AddDependency(accessor, true);
                return this;
            }
        }
    }
}